package nsip.components.ds;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

/**
 * An inventory which has a notion of time and can manage a demand in
 * a range of time invervals.
 * 
 * @author nico.rehwaldt
 */
public class TimeAwareInventory {

    /**
     * An enum denoting a time inverval
     */
    public enum Interval {

        IMMEDIATLY(new int[] {0, 1, 2, 3, 4}, 0.0),
        SHORT(new int[] {5, 6}, 0.0),
        MID(new int[] {7, 8}, 0.2),
        LONG(new int[] {9, 10}, 0.3),
        VERY_LONG(new int[] {11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, 0.4);
        
        private int[] days;
        private double reserve;
        
        /**
         * Creates a new time interval including the specified days
         * @param days
         */
        private Interval(int[] days, double reserve) {
            this.days = days;
            this.reserve = reserve;
        }

        /**
         * Return the days which are included in this time interval
         * @return
         */
        public int[] getDays() {
            return days;
        }

        /**
         * Return the due date for all orders in the specified interval
         * @return
         */
        public int getDueDate() {
            return days[0];
        }

        /**
         * Returns the amount of goods which should be ordered additionally
         * as a reserve when orders are made for the specified time interval.
         * 
         * @return
         */
        public double getReserve() {
            return reserve;
        }
    }
    
    private LinkedList<InventoryMap> inventories = new LinkedList();
    private int currentDay = 0;

    public void add(Integer[] partIDs, int quantity, int date) {
        getInventory(date).addAll(partIDs, quantity);
    }

    public void remove(int partID, int amount, int date) {
        int index = getDayIndex(date);

        // Add new inventories on the fly when a date is requested
        // which does not exist
        while (inventories.size() <= index) {
            inventories.add(new InventoryMap());
        }

        ListIterator<InventoryMap> iterator = inventories.listIterator(index);

        while (amount > 0 && iterator.hasNext()) {
            amount = iterator.next().remove(partID, amount);
        }

        if (amount > 0) {
            getInventory(date).add(partID, -amount);
        }
    }

    private InventoryMap getInventory(int day) {
        int index = getDayIndex(day);
        
        if (index < inventories.size()) {
            return inventories.get(index);
        } else {
            int counter = index - inventories.size();
            InventoryMap inventory = null;
            do {
                inventory = new InventoryMap();
                inventories.add(inventory);
            } while (counter-- > 0);
            return inventory;
        }
    }

    /**
     * Returns an entry set of productID / demand mappings for the given time
     * interval.
     * 
     * @param interval
     * @return
     */
    public Map<Integer, Integer> intervalMap(Interval interval) {
        InventoryMap map = new InventoryMap();
        
        for (int day: interval.getDays()) {
            for (Map.Entry<Integer, Integer> entry: getInventory(currentDay + day).entrySet()) {
                map.add(entry.getKey(), entry.getValue());
            }
        }

        return map;
    }
    
    /**
     * Return the demanded goods in the specified time interval
     * @param partID
     * @param interval
     * @return
     */
    public int get(int partID, Interval interval) {
        int demand = 0;

        for (int day: interval.getDays()) {
            demand += getInventory(currentDay + day).get(partID);
        }
        return demand;
    }

    /**
     * Perform update operations for next day
     * @param day
     */
    public void updateForNextDay(int day) {
        // Overflow todays demand to next day
        this.currentDay = day;
        InventoryMap today = inventories.poll();
        if (today != null) {
            InventoryMap tomorrow = getInventory(day);
            for (Map.Entry<Integer, Integer> entry: today.entrySet()) {
                tomorrow.add(entry.getKey(), entry.getValue());
            }

            updateNegativeDemands();
        }
    }


    private void updateNegativeDemands() {
        ListIterator<InventoryMap> iterator = inventories.listIterator();
        
        InventoryMap today = null;
        InventoryMap tomorrow = null;

        while (iterator.hasNext()) {
            if (today == null) {
                today = iterator.next();
                continue;
            }
            
            tomorrow = iterator.next();
            for (Map.Entry<Integer, Integer> entry: today.entrySet()) {
                int value = entry.getValue();
                if (value < 0) {
                    value = (value / 3 * 2);
                    
                    tomorrow.add(entry.getKey(), value);
                    today.add(entry.getKey(), -value);
                }
            }

            today = tomorrow;
        }
    }

    /**
     * Returns the index under which a specified day is stored
     * @param day
     * @return
     */
    private int getDayIndex(int day) {
        int index = day - currentDay;
        return index < 0 ? 0 : index;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("{ ");

        boolean first = true;
        
        for (Interval interval: Interval.values()) {
            Set<Map.Entry<Integer, Integer>> entries = intervalMap(interval).entrySet();
            
            if (!first) {
                builder.append(", ");
            } else {
                first = false;
            }

            builder.append(interval).append(": ").append(entries);
        }

        builder.append(" }");
        return builder.toString();
    }
    
    private class InventoryMap extends SimpleInventoryMap<Integer> {}
}
